﻿using DJLang.Exceptions;
using System;
using System.IO;
using System.Text;

namespace DJLang
{
    /// <summary>
    /// C#-style parser
    /// </summary>
    public class DJReader : IDisposable
    {
        private char[] buffer;
        private TextReader reader;

        public DJReader(TextReader reader)
        {
            buffer = new char[128];
            this.reader = reader;
        }

        public DJReader(string src)
        {
            buffer = new char[128];
            reader = new StringReader(src);
        }

        /// <summary>
        /// Evaluates dj source code stream to end. 
        /// 
        /// returns the evaluated last expression, null if stream is empty
        /// </summary>
        /// <param name="env">The Current DJ Environment</param>
        /// <exception cref="EvaluateDJErrException">throws when a DJErr was evaluated.</exception>
        public IDJValue EvaluateToEnd(DJEnv env)
        {
            IDJValue last = null;
            while (ReadValue() is IDJValue val)
            {
                if ((last = val.Evaluate(env)) is DJErr err)
                {
                    throw new EvaluateDJErrException(err);
                }
            }
            return last;
        }

        /// <summary>
        /// Reads a DJValue from TextReader.
        /// </summary>
        /// <returns>Parsed DJValue. null if EOF</returns>
        public IDJValue ReadValue()
        {
            SkipWhiteSpace();

            if (PeekChar() is char peek)
            {
                if (peek == ';')
                {
                    SkipComments();
                    SkipWhiteSpace();
                }

                if (PeekChar() is char next)
                {
                    IDJValue result = next switch
                    {
                        '(' => ReadExpr(),
                        '[' => ReadList(']'),
                        '{' => ReadList('}'),
                        '"' => ReadString(),
                        '@' => ReadSymbol(true),
                        '#' => ReadBool(),
                        _ => DJParser.SYMBOL_CHARACTERS.Contains(next) ? ReadSymbol() : new DJErr($"Unexcepted character:{next}(ASCII:{(int)next})"),
                    };

                    SkipWhiteSpace();

                    return result;
                }
            }

            return null;
        }

        public IDJValue ReadBool()
        {
            Skip(); // Skip '#'
            if (ReadChar() is char c)
            {
                return c switch
                {
                    't' => new DJBasic<bool>(true),
                    'f' => new DJBasic<bool>(false),
                    _ => new DJErr($"Syntax error, expect t/f got {c}"),
                };
            }
            return new DJErr("Unexcepted EOF");
        }

        public IDJValue ReadString()
        {
            if (ReadChar() is char fst && fst != '"') return new DJErr("Expect \"");
            var builder = new StringBuilder();

            while (ReadChar() is char c && c != '"')
            {
                if (c == '\\')
                {
                    if (ReadChar() is char next)
                    {
                        if (DJParser.UNESCAPABLE_CHARACTERS.Contains(next))
                        {
                            c = DJParser.GetUnescapeChar(next);
                        }
                        else
                        {
                            return new DJErr("Invalid escape seq \\" + next);
                        }
                    }
                    else
                    {
                        return new DJErr("Unexcepted EOF");
                    }
                }

                builder.Append(c);
                if (PeekChar() is not char) return new DJErr("Syntax error: string");
            }

            return new DJString(builder.ToString());
        }

        public IDJValue ReadList(char end)
        {
            if (ReadChar() is char fst && !"[{".Contains(fst)) return new DJErr("Expect [ or {");
            var result = new DJList();
            while (PeekChar() != end)
            {
                var val = ReadValue();
                if (val is DJErr e) return e;
                result.Push(val);

                if (reader.Peek() == -1) return new DJErr("Syntax error(list)");
            }
            Skip(); // skip ']' or '}'
            return result;
        }

        public IDJValue ReadExpr()
        {
            if (ReadChar() is char fst && fst != '(') return new DJErr("Expect (");

            var result = new DJExpr();
            while (PeekChar() != ')')
            {
                var val = ReadValue();
                if (val is DJErr e) return e;
                result.Push(val);

                if (reader.Peek() == -1) return new DJErr("Syntax error(expr)");
            }
            Skip(); // skip ')'
            return result;
        }

        public IDJValue ReadSymbol(bool quoted = false)
        {
            int len = 0;
            while (PeekChar() is char c && DJParser.SYMBOL_CHARACTERS.Contains(c))
            {
                reader.Read(buffer, len++, 1);
            }

            var symbol = new string(buffer, 0, len);

            if (int.TryParse(symbol, out int inum))
            {
                return new DJInt(inum);
            }
            else if (float.TryParse(symbol, out float fnum))
            {
                return new DJFloat(fnum);
            }
            else
            {
                return quoted ? new DJQuote(symbol) : new DJSymbol(symbol);
            }
        }

        private char? ReadChar()
        {
            int len = reader.Read(buffer, 0, 1);
            return len != 0 ? buffer[0] : null;
        }

        private char? PeekChar()
        {
            int i = reader.Peek();
            return i == -1 ? null : (char)i;
        }

        public void Skip() => reader.Read(buffer, 0, 1);

        public void SkipWhile(Func<char, bool> predicate)
        {
            int i;
            while ((i = reader.Peek()) != -1 && predicate((char)i))
            {
                reader.Read(buffer, 0, 1);
            }
        }

        public void SkipWhiteSpace() => SkipWhile(char.IsWhiteSpace);
        public string SkipComments()
        {
            Skip();
            return reader.ReadLine();
        }

        public void Dispose()
        {
            reader.Dispose();
        }
    }
}
